Runtime Flow

This guide documents Agentty's runtime data flows end to end: the foreground event loop, reducer/event buses, session-worker turn execution, merge/rebase/sync orchestration, and every background task with trigger points and side effects.

Architecture Goals🔗

Agentty runtime design is built around these constraints:

  • Keep domain logic independent from infrastructure and UI.
  • Keep long-running or external operations behind trait boundaries for testability.
  • Keep runtime event handling responsive by offloading background work to async tasks.
  • Keep AI-session changes isolated in git worktrees and reviewable as diffs.
  • Decouple agent transport (CLI subprocess vs app-server RPC) behind a unified channel abstraction.

Workspace Map🔗

PathResponsibility
crates/agentty/Main TUI application crate (agentty) with runtime, app orchestration, domain, infrastructure, and UI modules.
crates/ag-xtask/Workspace maintenance commands (index checks, migration checks, automation helpers).
docs/site/content/docs/End-user and contributor documentation published at /docs/.

Main Runtime Flow🔗

Primary foreground path from process start to one event-loop cycle:

main.rs
  ├─ Database::open(...)                    // sqlite open + WAL + FK + migrations
  ├─ RoutingAppServerClient::new()          // Codex/Gemini router
  ├─ App::new(...)
  │    ├─ load startup project/session snapshots
  │    ├─ fail unfinished operations from previous run
  │    └─ spawn app background tasks
  └─ runtime::run(&mut app)
       ├─ terminal::setup_terminal()
       ├─ event::spawn_event_reader(...)    // dedicated OS thread
       └─ run_main_loop(...)
            ├─ sessions.sync_from_handles() // pull Arc<Mutex> runtime state into snapshots
            ├─ ui::render::draw(...)
            └─ event::process_events(...)
                 ├─ key events -> mode handlers -> app/session orchestration
                 ├─ app events -> App::apply_app_events reducer
                 └─ tick -> refresh_sessions_if_needed safety poll

Foreground loop details:

  • run_main_loop() renders every cycle and applies snapshot sync before draw.
  • process_events() waits on terminal events, app events, or tick (tokio::select!).
  • After one event, it drains queued terminal events immediately to avoid one-key-per-frame lag.
  • Tick interval is 50ms; metadata-based session reload fallback is 5s (SESSION_REFRESH_INTERVAL).

Data Channels🔗

Agentty uses four primary runtime data channels:

ChannelProducer(s)Consumer(s)PayloadPurpose
Terminal Event channel (runtime/event.rs)Event-reader threadruntime::process_events()crossterm::EventUser input and terminal events.
App event bus (AppEvent)App background tasks, workers, task helpersApp::apply_app_events() reducerAppEvent variantsSafe cross-task app-state mutation.
Turn event stream (TurnEvent)AgentChannel implementationsSession worker consume_turn_events()Stream deltas/progress/pidReal-time turn output and progress updates.
Session handles (SessionHandles)Workers/session task helpersSessionState::sync_from_handles()Shared Arc<Mutex<...>> output/status/pidFast snapshot sync without full DB reload.

App Event Reducer Flow🔗

App::apply_app_events() is the single reducer path for async app events.

Flow:

  1. Drain queued events (first_event + try_recv loop).
  2. Reduce into AppEventBatch (coalesces refresh, git status, model/progress updates).
  3. Apply side effects in stable order.

Reducer behaviors that matter for data flow:

  • RefreshSessions sets should_force_reload, which triggers refresh_sessions_now() and reload_projects().
  • SessionUpdated marks touched sessions so reducer can call sync_session_from_handle() selectively.
  • SessionProgressUpdated updates transient progress labels used by UI.
  • AgentResponseReceived routes question-mode transitions for active view sessions.
  • After touched-session sync, terminal statuses (Done, Canceled) drop per-session worker senders so workers can shut down runtimes.

Session Turn Data Flow🔗

From prompt submit to persisted result:

  1. Prompt mode submits:
  2. start_session() for first prompt (TurnMode::Start) or reply() for follow-up (TurnMode::Resume).
  3. Session command is persisted in session_operation before enqueue.
  4. SessionWorkerService lazily creates or reuses a per-session worker queue.
  5. Worker marks operation running, checks cancel flags, then runs channel turn.
  6. Worker creates TurnRequest (reasoning level, model, prompt, replay output, provider conversation id).
  7. Worker spawns consume_turn_events() and sets initial progress (Thinking).
  8. AgentChannel::run_turn() streams TurnEvent values and returns TurnResult.
  9. Worker applies final result:
  10. Append final assistant transcript output when no assistant chunks were already streamed (answer text, fallback question text).
  11. Persist session questions and emit AppEvent::AgentResponseReceived.
  12. Persist stats and per-model usage.
  13. Persist provider conversation id (app-server providers).
  14. Run auto-commit assistance path.
  15. Refresh persisted session size.
  16. Update final status (Review or Question; on failure -> Review).

Operation Lifecycle and Recovery🔗

Turn execution is durable and restart-safe:

  • Before enqueue: insert session_operation row (queued).
  • Worker transitions: queued -> running -> done/failed/canceled.
  • Cancel requests are persisted and checked before command execution.
  • On startup, unfinished operations are failed with reason Interrupted by app restart, and impacted sessions are reset to Review.

Status Transition Rules🔗

Runtime status transitions enforced by Status::can_transition_to():

  • New -> InProgress (first prompt)
  • Review/Question -> InProgress (reply)
  • Review -> Queued -> Merging -> Done (merge queue path)
  • Review -> Rebasing -> Review/Question (rebase path)
  • Review/Question -> Canceled
  • InProgress/Rebasing -> Review/Question (post-turn or post-rebase)

Agent Channel Architecture🔗

Session workers are transport-agnostic through AgentChannel:

app/session/workflow/worker.rs
  └─ AgentChannel::run_turn(session_id, TurnRequest, event_tx)
       ├─ CliAgentChannel        (Claude; subprocess per turn)
       └─ AppServerAgentChannel  (Codex/Gemini; persistent runtime per session)
            └─ AppServerClient
                 └─ RoutingAppServerClient
                      ├─ RealCodexAppServerClient
                      └─ RealGeminiAcpClient

Key types (infra/channel.rs):

TypePurpose
TurnRequestInput payload: reasoning_level, folder, live_session_output, model, mode (start/resume), prompt, provider_conversation_id.
TurnEventIncremental stream events: AssistantDelta, ThoughtDelta, Progress, Completed, Failed, PidUpdate.
TurnResultNormalized output: assistant_message, token counts, provider_conversation_id.
TurnModeStart (fresh turn) or Resume (with optional session output replay).

Provider conversation id flow:

  • App-server providers return provider_conversation_id in TurnResult.
  • Worker persists it to DB (update_session_provider_conversation_id).
  • Future TurnRequest loads and forwards it so runtime restarts can resume native provider context.

Agent Interaction Protocol Flow🔗

Provider output is normalized to one structured response protocol:

  1. Prompt builders prepend protocol instructions (answer/question schema).
  2. Channels stream deltas/progress as TurnEvent.
  3. Final output is parsed to protocol messages.
  4. Worker persists final display text and question payloads, then emits AgentResponseReceived.

Streaming behavior differs by transport/provider:

  • CLI channel (CliAgentChannel): parses stdout lines into AssistantDelta and Progress; keeps raw output for final parse.
  • App-server channel (AppServerAgentChannel): bridges AppServerStreamEvent to TurnEvent.
  • Codex thought phases (thinking/plan/reasoning/thought) stream as ThoughtDelta.
  • Strict providers suppress streamed assistant chunks when needed so malformed first-pass protocol JSON is not persisted.
  • Worker persistence behavior: streamed ThoughtDelta and Progress updates drive transient progress badges and are not appended to session transcript output.

Final-output validation:

  • Claude and Gemini use strict protocol parsing with up to three repair retries when invalid.
  • Codex uses permissive parse fallback (schema already supplied via app-server outputSchema path).

Clarification Question Loop🔗

Question-mode loop:

  1. Worker receives final parsed response containing question messages.
  2. Worker persists question list and sets session status Question.
  3. Reducer switches active view to AppMode::Question when that session is focused.
  4. User answers each question.
  5. Runtime builds one follow-up prompt:
Clarifications:
1. Q: <question 1>
   A: <response 1>
2. Q: <question 2>
   A: <response 2>
  1. Runtime submits this as a normal reply turn; flow returns to standard worker path.

Background Task Catalog🔗

Detached/background execution paths and their trigger conditions:

TaskTriggerSpawn siteEmits / WritesWhat it does
Terminal event reader threadRuntime startupruntime/event::spawn_event_readerTerminal Event channelPolls crossterm and forwards events; pauses while external editor is open.
Git status poller loopApp startup (if project has git branch), and project switchTaskService::spawn_git_status_taskAppEvent::GitStatusUpdatedPeriodic fetch + ahead/behind snapshot (about every 30s).
Version check one-shotApp startupTaskService::spawn_version_check_taskAppEvent::VersionAvailabilityUpdatedChecks npm latest version tag and reports update availability.
Per-session worker loopFirst command enqueue for a sessionSessionWorkerService::spawn_session_workerDB session_operation updates, app/session updatesSerializes all turn commands per session and manages channel lifecycle.
Per-turn turn-event consumerEvery queued turn executionrun_channel_turnOutput append, progress updates, pid slot updatesConsumes TurnEvent stream and applies immediate side effects.
CLI stdout/stderr readersEvery CLI-backed turnCliAgentChannel::run_turnTurnEvent stream + raw buffersReads subprocess streams and emits incremental deltas/progress.
App-server stream bridgeEvery app-server-backed turnAppServerAgentChannel::run_turnTurnEvent streamBridges AppServerStreamEvent to unified turn events.
Session title generationFirst Start turn, before main turn executionspawn_start_turn_title_generationDB title + AppEvent::RefreshSessionsRuns one-shot title prompt in background and persists generated title if valid.
At-mention file indexingPrompt input activates @ mention moderuntime/mode/prompt::activate_at_mentionAppEvent::AtMentionEntriesLoadedLists session files (spawn_blocking) and updates mention picker entries.
Background session-size refreshEnter on session in list modeApp::refresh_session_size_in_backgroundDB size + AppEvent::RefreshSessionsComputes diff-size bucket without blocking key handling path.
Deferred session cleanupDelete with deferred cleanup pathdelete_selected_session_deferred_cleanupFilesystem/git side effectsRemoves worktree folder and branch asynchronously after DB deletion.
Focused review assistView mode focused-review toggle when diff is reviewableTaskService::spawn_focused_review_assist_taskFocusedReviewPrepared / FocusedReviewPreparationFailedRuns model review prompt and stores final review text or error.
Sync-main workflow taskList-mode sync action (s)TokioSyncMainRunner::start_sync_mainAppEvent::SyncMainCompletedPull-rebase/push selected project branch, with assisted conflict flow.
Session merge taskMerge confirmation acceptedSessionMergeService::merge_sessionOutput append, status updates, session metadata updatesRuns rebase + squash merge + worktree cleanup in background.
Session rebase taskRebase action in view modeSessionMergeService::rebase_sessionOutput append, status updatesRuns assisted rebase and returns session to Review/Question.

Sync, Merge, and Rebase Flows🔗

Project and session git workflows use shared boundaries (GitClient, FsClient, assist helpers) but have distinct orchestration paths:

  • sync main: selected project branch pull/rebase/push, optional assisted conflict resolution, popup result summary.
  • session merge: queue-aware workflow, assisted rebase first, squash merge into base branch, worktree cleanup, status Done on success.
  • session rebase: assisted rebase of session branch onto base branch, returns to Review after completion/failure reporting.

Persistence and Recovery Boundaries🔗

Persistence invariants that shape runtime flow:

  • DB opens with SQLite WAL and foreign_keys = ON, then embedded migrations run at startup.
  • Session snapshots in memory are authoritative for rendering; DB is authoritative for restart recovery.
  • Shared session handles (output, status, child_pid) provide low-latency updates between DB reloads.
  • Event-driven refresh is primary (RefreshSessions); metadata polling is fallback safety only.
  • External integrations (GitClient, AppServerClient, AgentChannel, EventSource, FsClient, TmuxClient) isolate side effects and enable deterministic tests.